Managing Geometry Properties of Imported Networks

The Imported geometry class is used to store the geometrical properties of imported networks. When importing an extracted network into OpenPNM using any of the io classes, all the geometrical and topological properties are lumped together on the network object. OpenPNM is generally designed such that geometrical properties are stored on a geometry object, so this class address this issue. The main function of the Imported class is to automatically strip the geometrical properties off of the network and transfer them onto itself.

What problem does the Imported class solve? Although OpenPNM can function with the geometrical properties on the network, a problem arises if the user wishes to add more pores to the network, such as boundary pores. In this case, they will probably wish to add pore-scale models to calculate size information, say 'pore.volume'. If they add this to the network, this model will overwrite the pre-existing 'pore.volume' values. The solution to this problem is an intrinsic part of OpenPNM: create a separate geometry object to manage it's own 'pore.volume' model and values. However, this won't work! OpenPNM will not allow an array called 'pore.volume' to exist on the network and a geometry object. The reason is that networks store values for every pore, so when adding new pores the network the 'pore.volume' array will increase to accommodate them. If you attempt to put 'pore.volume' values on the geometry object, you're are essentially putting two values in those locations. Therefore, the Imported class solves this problem by first transferring the 'pore.volume' array (and all other geometrical properties) from the network to itself.


In [1]:
import numpy as np
import openpnm as op
ws = op.Workspace()
ws.settings['loglevel'] = 50  # Supress warnings, but see error messages

Let's start by generating a random network using the Delaunay class. This will repreent an imported network:


In [2]:
np.random.seed(0)
pn = op.network.Delaunay(shape=[1, 1, 0], num_points=100)

This network generator adds nicely defined boundary pores around the edges/faces of the network. Let's remove these for the sake of this example:


In [3]:
op.topotools.trim(network=pn, pores=pn.pores('boundary'))

In [4]:
fig = op.topotools.plot_coordinates(network=pn, c='r')
fig = op.topotools.plot_connections(network=pn, fig=fig)
fig.set_size_inches((5, 5))


This network does not have any geometrical properties on it when generated. To mimic the situation of an imported network, let's manually enter some values for 'pore.diameter'. We'll just assign random numbers to illustrate the point:


In [5]:
pn['pore.diameter'] = np.random.rand(pn.Np)

Now when we print the network we'll see all the topological data ('pore.coords' and 'throat.conns'), all the labels that were added by the generator (e.g. 'pore.left'), as well as the new geometry info we just added ('pore.diameter'):


In [6]:
print(pn)


――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
openpnm.network.Delaunay : net_01
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
#     Properties                                    Valid Values
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
1     pore.coords                                     100 / 100  
2     pore.diameter                                   100 / 100  
3     throat.conns                                    270 / 270  
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
#     Labels                                        Assigned Locations
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
1     pore.all                                      100       
2     pore.back                                     0         
3     pore.bottom                                   100       
4     pore.boundary                                 0         
5     pore.front                                    0         
6     pore.internal                                 100       
7     pore.left                                     0         
8     pore.right                                    0         
9     pore.surface                                  26        
10    pore.top                                      100       
11    throat.all                                    270       
12    throat.boundary                               0         
13    throat.internal                               270       
14    throat.surface                                0         
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――

OpenPNM was designed to work by assigning geomtrical information to Geometry objects. The presence of 'pore.diameter' on the network can be a problem in some cases. For instance, let's add some boundary pores to the left edge:


In [7]:
Ps = pn['pore.surface']*(pn['pore.coords'][:, 0] < 0.1)
Ps = pn.toindices(Ps)
op.topotools.add_boundary_pores(network=pn, pores=Ps, 
                                move_to=[0, None, None], 
                                apply_label='left')

Visualizing this networks shows the newl added pores where we intended:


In [8]:
fig = op.topotools.plot_coordinates(network=pn, pores=pn.pores('left', mode='not'), c='r')
fig = op.topotools.plot_coordinates(network=pn, pores=pn.pores('left'), c='g', fig=fig)
fig = op.topotools.plot_connections(network=pn, fig=fig)
fig.set_size_inches((7, 7))


Now we have internal pores (red) and boundary pores (green). We would like to assign geometrical information to the boundary pores that we just created. This is typically done by creating a Geometry object, then either assigning numerical values or attaching a pore-scale model that calculates the values. The problem is that OpenPNM prevents you from having 'pore.diameter' on the network AND a geometry object at the same time.


In [9]:
Ps = pn.pores('left')
Ts = pn.find_neighbor_throats(pores=Ps)
geo_bndry = op.geometry.GenericGeometry(network=pn, pores=Ps, throats=Ts)

Now we we try to assign 'pore.diameter', we'll get the following exception (The "try-except" structure is used for the purpose of this notebook example, but is not needed in an actual script):


In [10]:
try:
    geo_bndry['pore.diameter'] = 0
except Exception as e:
    print(e)


Cannot create pore.diameter when pore.diameter is already defined

The solution is to remove the geometrical information from the network before adding the boundary pores, and place them on their own geometry. In this example it is easy to transfer the 'pore.diameter' array, but in the case of a real extracted network there could be quite a few arrays to move. OpenPNM has a facility for doing this: the Imported geometry class.

Using the Imported Geometry Class

Let's create a network and add a geometric properties again, this time before adding boundary pores.


In [11]:
pn = op.network.Delaunay(shape=[1, 1, 0], num_points=100)
pn['pore.diameter'] = np.random.rand(pn.Np)

Here we pass the network to the Imported geometry class. This class literally removes all numerical data from the network to itself. Everything is moved except topological info ('pore.coords' and 'throat.conns') and labels ('pore.left').


In [12]:
geo = op.geometry.Imported(network=pn)

Printing geo reveals that the 'pore.diameter' array has been transferred from the network automatically:


In [13]:
print(geo)


――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
openpnm.geometry.Imported : geo_01
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
#     Properties                                    Valid Values
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
1     pore.area                                       135 / 135  
2     pore.diameter                                   135 / 135  
3     pore.volume                                     135 / 135  
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
#     Labels                                        Assigned Locations
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
1     pore.all                                      135       
2     throat.all                                    301       
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――

Now that the geometrical information is properly assigned to a geometry object, we can now use OpenPNM as intended. Let's extend this network by adding a single new pore.


In [14]:
op.topotools.extend(network=pn, pore_coords = [[1.2, 1.2, 0]], labels='new')

The new pore can clearly be seen outside the top-right corner of the domain.


In [15]:
fig = op.topotools.plot_coordinates(network=pn, pores=pn.pores('left', mode='not'), c='r')
fig = op.topotools.plot_coordinates(network=pn, pores=pn.pores('left'), c='g', fig=fig)
fig = op.topotools.plot_connections(network=pn, fig=fig)
fig.set_size_inches((7, 7))


We can now create a geometry just for this single pore and we will be free to add any properties we wish:


In [16]:
geo2 = op.geometry.GenericGeometry(network=pn, pores=pn.pores('new'))
geo2['pore.diameter'] = 2.0

In [17]:
print(geo2)


――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
openpnm.geometry.GenericGeometry : geo_02
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
#     Properties                                    Valid Values
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
1     pore.diameter                                     1 / 1    
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
#     Labels                                        Assigned Locations
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
1     pore.all                                      1         
2     throat.all                                    0         
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――

Note that the network has the ability to fetch the 'pore.diameter' array from the geometry sub-domain object and create a single full array containing the values from all the locations. In the printout below we can see the value of 2.0 in the very last element, which is where new pores are added to the list.


In [18]:
print(pn['pore.diameter'])


[0.81083862 0.34819194 0.2114548  0.05938319 0.87602685 0.91854645
 0.12012018 0.33447374 0.17537207 0.11589847 0.89986674 0.05687726
 0.98048566 0.09645086 0.86347065 0.56650611 0.36791749 0.34234238
 0.75736414 0.3145733  0.65731892 0.51732608 0.48496565 0.90116217
 0.55464506 0.8268616  0.72557353 0.03855725 0.77311005 0.21687025
 0.90314965 0.04292419 0.33307203 0.09973295 0.47558912 0.82002244
 0.29818736 0.1509349  0.33026704 0.81388014 0.14038396 0.22736245
 0.06885196 0.70571004 0.39523324 0.31083998 0.71862639 0.33597754
 0.72777127 0.8151994  0.21766284 0.9738187  0.16235795 0.29084091
 0.17979529 0.34550566 0.48006089 0.52217587 0.85360604 0.88944791
 0.22010386 0.62289403 0.11149606 0.45896986 0.32233354 0.31650075
 0.48258424 0.72982764 0.06918266 0.87917334 0.73481377 0.17649939
 0.93916091 0.50631222 0.99980858 0.19725947 0.5349082  0.29024804
 0.30417356 0.59106538 0.92171907 0.80526386 0.7239414  0.55917378
 0.9222985  0.49236141 0.87383218 0.83398164 0.21383535 0.77122546
 0.01217116 0.32282954 0.22956744 0.50686296 0.73685316 0.09767637
 0.5149222  0.93841202 0.22864655 0.67714114 0.59288027 0.0100637
 0.4758262  0.70877039 0.04397543 0.87952148 0.52008142 0.03066105
 0.22441361 0.9536757  0.58231973 0.10747257 0.2875445  0.45670363
 0.02095007 0.41161551 0.48945864 0.24367788 0.588639   0.75324012
 0.23583422 0.6204999  0.63962224 0.9485403  0.77827617 0.84834527
 0.49041991 0.18534859 0.99581529 0.12935576 0.47145732 0.0680931
 0.94385086 0.96492494 0.71938906 2.        ]